
package org.apache.solr.response;

import java.io.IOException;
import java.io.Writer;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.XML;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.ReturnFields;

/**
 * @lucene.internal
 */
public class XMLWriter extends TextResponseWriter {

    public static float CURRENT_VERSION = 2.2f;
    private static final char[] XML_START1 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".toCharArray();
    private static final char[] XML_STYLESHEET = "<?xml-stylesheet type=\"text/xsl\" href=\"".toCharArray();
    private static final char[] XML_STYLESHEET_END = "\"?>\n".toCharArray();

    /*
     private static final char[] XML_START2_SCHEMA=(
     "<response xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
     +" xsi:noNamespaceSchemaLocation=\"http://pi.cnet.com/cnet-search/response.xsd\">\n"
     ).toCharArray();
     ***/
    private static final char[] XML_START2_NOSCHEMA = ("<response>\n").toCharArray();
    final int version;

    public static void writeResponse(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {

        XMLWriter xmlWriter = null;
        try {
            xmlWriter = new XMLWriter(writer, req, rsp);
            xmlWriter.writeResponse();
        }
        finally {
            xmlWriter.close();
        }
    }

    public XMLWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) {
        super(writer, req, rsp);

        String lversion = req.getParams().get(CommonParams.VERSION);
        float ver = lversion == null ? CURRENT_VERSION : Float.parseFloat(lversion);
        this.version = (int)(ver * 1000);
        if(this.version < 2200) {
            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "XMLWriter does not support version: " + lversion);
        }
    }

    public void writeResponse() throws IOException {
        writer.write(XML_START1);

        String stylesheet = req.getParams().get("stylesheet");
        if(stylesheet != null && stylesheet.length() > 0) {
            writer.write(XML_STYLESHEET);
            XML.escapeAttributeValue(stylesheet, writer);
            writer.write(XML_STYLESHEET_END);
        }

        /*
         String noSchema = req.getParams().get("noSchema");
         // todo - change when schema becomes available?
         if (false && noSchema == null)
         writer.write(XML_START2_SCHEMA);
         else
         writer.write(XML_START2_NOSCHEMA);
         ***/
        writer.write(XML_START2_NOSCHEMA);

        // dump response values
        NamedList<?> lst = rsp.getValues();
        Boolean omitHeader = req.getParams().getBool(CommonParams.OMIT_HEADER);
        if(omitHeader != null && omitHeader) {
            lst.remove("responseHeader");
        }
        int sz = lst.size();
        int start = 0;

        for(int i = start; i < sz; i++) {
            writeVal(lst.getName(i), lst.getVal(i));
        }

        writer.write("\n</response>\n");
    }

    /**
     * Writes the XML attribute name/val. A null val means that the attribute is missing.
     */
    private void writeAttr(String name, String val) throws IOException {
        writeAttr(name, val, true);
    }

    public void writeAttr(String name, String val, boolean escape) throws IOException {

        if(val != null) {
            writer.write(' ');
            writer.write(name);
            writer.write("=\"");
            if(escape) {
                XML.escapeAttributeValue(val, writer);
            }
            else {
                writer.write(val);
            }
            writer.write('"');
        }
    }

    void startTag(String tag, String name, boolean closeTag) throws IOException {

        if(doIndent) {
            indent();
        }

        writer.write('<');
        writer.write(tag);
        if(name != null) {
            writeAttr("name", name);
            if(closeTag) {
                writer.write("/>");
            }
            else {
                writer.write(">");
            }
        }
        else {
            if(closeTag) {
                writer.write("/>");
            }
            else {
                writer.write('>');
            }
        }
    }

    @Override
    public void writeStartDocumentList(String name, long start, int size, long numFound, Float maxScore) throws IOException {

        if(doIndent) {
            indent();
        }

        writer.write("<result");
        writeAttr("name", name);
        writeAttr("numFound", Long.toString(numFound));
        writeAttr("start", Long.toString(start));
        if(maxScore != null) {
            writeAttr("maxScore", Float.toString(maxScore));
        }
        writer.write(">");

        incLevel();
    }

    /**
     * The SolrDocument should already have multivalued fields implemented as
     * Collections -- this will not rewrite to &lt;arr&gt;
     */
    @Override
    public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException {

        startTag("doc", name, false);
        incLevel();

        for(String fname : doc.getFieldNames()) {
            if(!returnFields.wantsField(fname)) {
                continue;
            }

            Object val = doc.getFieldValue(fname);
            if("_explain_".equals(fname)) {
                System.out.println(val);
            }
            writeVal(fname, val);
        }

        decLevel();
        writer.write("</doc>");
    }

    @Override
    public void writeEndDocumentList() throws IOException {

        decLevel();
        if(doIndent) {
            indent();
        }
        writer.write("</result>");
    }

    // Generic compound types
    //
    @Override
    public void writeNamedList(String name, NamedList val) throws IOException {

        int sz = val.size();
        startTag("lst", name, sz <= 0);

        incLevel();
        for(int i = 0; i < sz; i++) {
            writeVal(val.getName(i), val.getVal(i));
        }
        decLevel();

        if(sz > 0) {
            if(doIndent) {
                indent();
            }
            writer.write("</lst>");
        }
    }

    @Override
    public void writeMap(String name, Map map, boolean excludeOuter, boolean isFirstVal) throws IOException {

        int sz = map.size();

        if(!excludeOuter) {
            startTag("lst", name, sz <= 0);
            incLevel();
        }

        for(Map.Entry entry : (Set<Map.Entry>)map.entrySet()) {
            Object k = entry.getKey();
            Object v = entry.getValue();
            // if (sz<indentThreshold) indent();
            writeVal(null == k ? null : k.toString(), v);
        }

        if(!excludeOuter) {
            decLevel();
            if(sz > 0) {
                if(doIndent) {
                    indent();
                }
                writer.write("</lst>");
            }
        }
    }

    @Override
    public void writeArray(String name, Object[] val) throws IOException {
        writeArray(name, Arrays.asList(val).iterator());
    }

    @Override
    public void writeArray(String name, Iterator iter) throws IOException {

        if(iter.hasNext()) {
            startTag("arr", name, false);
            incLevel();
            while(iter.hasNext()) {
                writeVal(null, iter.next());
            }
            decLevel();
            if(doIndent) {
                indent();
            }
            writer.write("</arr>");
        }
        else {
            startTag("arr", name, true);
        }
    }

    // Primitive types
    //
    @Override
    public void writeNull(String name) throws IOException {
        writePrim("null", name, "", false);
    }

    @Override
    public void writeStr(String name, String val, boolean escape) throws IOException {
        writePrim("str", name, val, escape);
    }

    @Override
    public void writeInt(String name, String val) throws IOException {
        writePrim("int", name, val, false);
    }

    @Override
    public void writeLong(String name, String val) throws IOException {
        writePrim("long", name, val, false);
    }

    @Override
    public void writeBool(String name, String val) throws IOException {
        writePrim("bool", name, val, false);
    }

    @Override
    public void writeFloat(String name, String val) throws IOException {
        writePrim("float", name, val, false);
    }

    @Override
    public void writeFloat(String name, float val) throws IOException {
        writeFloat(name, Float.toString(val));
    }

    @Override
    public void writeDouble(String name, String val) throws IOException {
        writePrim("double", name, val, false);
    }

    @Override
    public void writeDouble(String name, double val) throws IOException {
        writeDouble(name, Double.toString(val));
    }

    @Override
    public void writeDate(String name, String val) throws IOException {
        writePrim("date", name, val, false);
    }

    //
    // OPT - specific writeInt, writeFloat, methods might be faster since
    // there would be less write calls (write("<int name=\"" + name + ... + </int>)
    //
    private void writePrim(String tag, String name, String val, boolean escape) throws IOException {

        int contentLen = val == null ? 0 : val.length();

        startTag(tag, name, contentLen == 0);
        if(contentLen == 0) {
            return;
        }

        if(escape) {
            XML.escapeCharData(val, writer);
        }
        else {
            writer.write(val, 0, contentLen);
        }

        writer.write('<');
        writer.write('/');
        writer.write(tag);
        writer.write('>');
    }
}
